Skip to content

DF-26795: apply RDD symbol overrides in coinmetrics-lwba - #5380

Open
cl-efornaciari wants to merge 24 commits into
mainfrom
feature/DF-26795/coinmetrics-lwba-request-overrides
Open

cl-efornaciari wants to merge 24 commits into
mainfrom
feature/DF-26795/coinmetrics-lwba-request-overrides

Conversation

@cl-efornaciari

@cl-efornaciari cl-efornaciari commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Description

The coinmetrics-lwba adapter silently discards every RDD symbol override. It has done so since the clone hack was introduced, so RDD has been correct while the adapter ignored it.

packages/sources/coinmetrics-lwba/src/index.ts builds its endpoint by cloning the one exported from the coinmetrics package:

const newEndpoint = Object.assign(Object.create(Object.getPrototypeOf(endpoint)), endpoint)

Object.assign copies requestTransforms by reference. symbolOverrider was bound to the original endpoint in the AdapterEndpoint constructor, but only the clone is ever initialize()d — so the inherited binding reads this.adapterName as undefined, the override lookup misses, and the override is dropped with no error.

Only request overrides break. Hardcoded endpoint overrides and adapterNameOverride don't depend on this.adapterName, so they still work — which is part of why this stayed invisible.

Impact

A full scan of reference-data-directory finds 2 live feeds affected — the only live feeds carrying a semantically meaningful coinmetrics-lwba override:

feed network address override failure rate (24h)
FXS / USD mainnet 0xc1c54D09554Cf65d74a289805c2731cf6EA57373 FRAX → frax_frax 64.8%
MATIC / USD sepolia-arbitrum 0xE6616a09e029D477183CbA6BB6133643b522C15c MATIC → pol 100%

Nine other feeds carry an override block but are dead/inactive, and most of those are case-only no-ops (POL → pol) that the transport's toLowerCase() already handles.

Why the wrong symbol matters

frax and frax_frax are the same asset (~$0.29) but CoinMetrics prices them off disjoint venue sets:

  • frax_frax — Binance, Bybit, Bitget, Gate.io, MEXC. Deep books, ~0.20% spread, streams continuously.
  • frax — Poloniex, HitBTC, LBank, Huobi, Gemini, Bithumb, KuCoin. ask_size as low as 46, ~0.88% spread, and absent 59% of the time.

cache_data_set_count_total over 24h, where full rate is 1,382,400 writes/day:

symbol writes/24h % of full rate 504 rate
eth, link, pol, steth 1,381,852 100% 0%
dai 1,290,844 93.4% 4.8%
frax 561,736 40.6% 64.8%
matic 0 (zero over 7 days) 0% 100%

Coverage predicts failure almost 1:1. matic is the control: same adapter, same socket — CoinMetrics has not published it once in 7 days, while pol (sent directly by mainnet feeds, no override needed) runs at 0% failure.

Changes

  • Replace the clone with a fresh LwbaEndpoint constructed from endpointParameters (now exported from coinmetrics/src/endpoint/lwba.ts). The AdapterEndpoint constructor binds requestTransforms (symbolOverrider) to the instance it creates, and only this instance is ever initialize()d — so overrides resolve against the initialized adapter name. Fresh aliases/requestTransforms arrays also end the shared-array mutation the clone relied on.
  • Keep the result = mid mapping in customOutputValidation, passed as a constructor param: framework ≥ 2.19.1 chains it ahead of the built-in bid ≤ mid ≤ ask invariant check, which still runs when this returns undefined. (The mapping is required on every framework version — LWBA responses type Result: null and nothing else populates it.)
  • Bump @chainlink/external-adapter-framework 2.17.1 → 2.20.0 across the 13 coupled packages (details below).
  • Add test/integration/adapter-ws-overrides.test.ts asserting the subscribed URL contains assets=frax_frax and that the overridden symbol resolves a price; add a FRAX/USD request with the override block to test-payload.json.
  • Update six LWBA invariant-violation snapshots and one hardcoded cfbenchmarks status assertion for the 2.19.1 resultValidator behavior; add the opt-in MAX_WS_CONNECTION_AGE_SECONDS key to tiingo's stubbed unit-test settings (2.20.0).

Framework bump: 2.17.1 → 2.20.0 (13 packages)

The Check that changed adapters have the latest EA framework version job requires changed adapters to pin latest.

  • coinmetrics and coinmetrics-lwba move together. coinmetrics-lwba imports its endpoint from the coinmetrics package; a split pin loads two framework copies under PnP and startup fails (The logger factory provider does not have a factory set) — the new copy's start() sets the logger factory on its own module singleton, and the old copy never receives one. Reproduces in the test suite and would fail identically in production.

  • token-allocation's v3 adapter set moves together. It aggregates members into a single PriceAdapter<any>[], so every member must be compiled against the same framework copy (nominal type identity):

    Type 'PriceAdapterRequest<...framework-npm-2.20.0...>' is not assignable to
    Type 'PriceAdapterRequest<...framework-npm-2.17.1...>'
    

    So blocksize-capital, cfbenchmarks, coingecko, coinmarketcap, coinpaprika, cryptocompare, finage, kaiko, ncfx, tiingo and token-allocation (devDependency) move with the two coinmetrics packages.

Behaviour change: LwbaEndpoint.resultValidator (2.19.1, #876)

invariant-violating quote
2.17.1 500 AdapterLWBAError thrown on the read path (and the bad quote cached, so every request 500s until a valid one replaces it)
≥2.19.1 rejected at cache-write time: 502 where no valid quote is cached, 200 with the last known-good quote otherwise

Six snapshots and the cfbenchmarks assertion record this. This path has never fired in production — over 7 days across both adapters, zero 500s in ~68M requests:

adapter 200 400 504 500
coinmetrics 30,454,039 100,820 11,782,134 0
coinmetrics-lwba 25,605,817 0 112,521 0

The change is also arguably an improvement: the bad tick is dropped at the write boundary, the last good quote stands, and its original providerIndicatedTimeUnixMs is preserved so staleness remains visible downstream.

2.20.0 (#901)

Adds opt-in MAX_WS_CONNECTION_AGE_SECONDS (proactive WS reconnect; disabled by default, no behaviour change unless configured). Only test impact: tiingo's stock_quotes unit test stubs adapterSettings and needs the new key, mirroring #901's own generator-template fix.

Steps to Test

Automated — the new integration test captures the URL the adapter opens against the mocked WS provider and asserts on assets=. Verified it fails for the right reason by temporarily reverting the fix:

subscribed URL request
without fix ...?assets=frax&api_key=... 504
with fix ...?assets=frax_frax&api_key=... 200

That reproduces the production symptom exactly: the mock streams frax_frax-usd, so with the override dropped nothing ever lands under the requested cache key.

Full suite across the 13 changed packages (Node 20, METRICS_ENABLED=false to match CI): 31 suites / 234 tests / 142 snapshots passed. Repo-wide yarn setup clean, yarn install --immutable clean, eslint 0 errors, prettier clean.

Manual:

curl -s -X POST localhost:8080 -H 'Content-Type: application/json' \
  -d '{"data":{"endpoint":"crypto","from":"FRAX","to":"USD","overrides":{"coinmetrics-lwba":{"FRAX":"frax_frax"}}}}' | jq

meta.metrics.feedId is computed post-transform, so it reads "base":"frax_frax" with the fix and "base":"frax" without.

Two local-run notes: tests require Node 20.6+ (pino 10.3.1 calls diagnostics_channel.tracingChannel), and snapshots must be generated with METRICS_ENABLED=false (CI sets it; locally it defaults to true, which adds a meta{} block to responses).

Rollout note

Deploying changes the generated WS URL, so the transport closes and reopens the shared connection (websocket.ts) — a brief one-time blip across all feeds on this adapter as it resubscribes with frax_frax in place of frax. 2.20.0's MAX_WS_CONNECTION_AGE_SECONDS is opt-in and unset, so it adds no new connection churn.

Confirm afterwards via cache_data_set_count_total for the frax_frax feed_id; it should climb toward the ~1.38M/day full rate. Coverage for frax_frax on the asset-quotes product is currently supported by a 2-minute local sample plus full coverage on the REST reference-rate product, so the post-deploy metric is the real confirmation.

Quality Assurance

  • If a new adapter was made, or an existing one was modified so that its environment variables have changed, update the relevant infra-k8s configuration file. — no env var changes
  • If a new adapter was made, or an existing one was modified so that its environment variables have changed, update the relevant adapter-secrets configuration file. — no env var changes
  • If a new adapter was made, or a new endpoint was added, update the test-payload.json file with relevant requests.
  • The branch naming follows git flow or is created from Jira.
  • This is related to a maximum of one Jira story or GitHub issue.
  • Types are safe.
  • All code changes have 100% unit and integration test coverage.

🤖 Generated with Claude Code

…pped

The adapter clones the endpoint exported by the coinmetrics package with
Object.assign, which copies `requestTransforms` by reference. `symbolOverrider`
stays bound to the original endpoint, which is never initialize()d, so
`this.adapterName` reads as undefined, the override lookup misses, and every
RDD symbol override is silently discarded.

Give the clone its own `aliases` and `requestTransforms` arrays and rebind
`symbolOverrider` to it.

Affects two live feeds:

- FXS/USD (mainnet, 0xc1c54D09554Cf65d74a289805c2731cf6EA57373) subscribed to
  `frax` instead of `frax_frax`. CoinMetrics prices `frax` off second-tier
  venues and publishes it only ~41% of the time, so ~65% of requests 504.
- MATIC/USD (sepolia-arbitrum, 0xE6616a09e029D477183CbA6BB6133643b522C15c)
  subscribed to `matic` instead of `pol`. CoinMetrics has not published
  `matic` in 7 days, so it fails 100%.

DF-26795

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fbfdf0f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 15 packages
Name Type
@chainlink/coinmetrics-lwba-adapter Patch
@chainlink/coinmetrics-adapter Patch
@chainlink/blocksize-capital-adapter Patch
@chainlink/cfbenchmarks-adapter Patch
@chainlink/coingecko-adapter Patch
@chainlink/coinmarketcap-adapter Patch
@chainlink/coinpaprika-adapter Patch
@chainlink/cryptocompare-adapter Patch
@chainlink/finage-adapter Patch
@chainlink/kaiko-adapter Patch
@chainlink/ncfx-adapter Patch
@chainlink/tiingo-adapter Patch
@chainlink/token-allocation-adapter Patch
@chainlink/savax-price-adapter Patch
@chainlink/xsushi-price-adapter Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

cl-efornaciari and others added 4 commits September 10, 2026 13:16
CI requires changed adapters to pin the latest ea-framework. Bumping only
coinmetrics-lwba is not viable: it imports its endpoint from the coinmetrics
package, so a split pin loads two framework copies. Adapter.initialize (2.19.1)
then calls endpoint.initialize (2.17.1), whose logger factory singleton was
never set, and startup fails with "The logger factory provider does not have a
factory set". Both packages therefore move together.

2.19.1 adds LwbaEndpoint.resultValidator, which rejects invariant-violating
quotes at cache-write time instead of throwing on the response path. Behaviour
for a violating quote changes from a 500 AdapterLWBAError to a 200 carrying the
last known-good quote, since the bad quote no longer reaches the cache. Two
snapshots are updated to record this.

DF-26795

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
token-allocation aggregates its v3 adapters into a single PriceAdapter<any>[],
so every member must be built against the same framework copy. Bumping
coinmetrics alone broke nominal type identity for the array:

  Type 'PriceAdapterRequest<...framework-npm-2.19.1...>' is not assignable to
  Type 'PriceAdapterRequest<...framework-npm-2.17.1...>'

So token-allocation and the remaining ten members move to 2.19.1 together:
blocksize-capital, cfbenchmarks, coingecko, coinmarketcap, coinpaprika,
cryptocompare, finage, kaiko, ncfx, tiingo.

2.19.1 adds LwbaEndpoint.resultValidator, which rejects invariant-violating
quotes at cache-write time rather than throwing on the read path. A violating
quote now yields 502, or 200 with the last known-good quote where one was
already cached, instead of 500. Four snapshots and one hardcoded status
assertion in cfbenchmarks are updated to record this.

This path has never fired in production: zero 500s across ~68M requests over
7 days on both coinmetrics adapters.

DF-26795

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cl-efornaciari
cl-efornaciari force-pushed the feature/DF-26795/coinmetrics-lwba-request-overrides branch from ae62e1f to dc45be9 Compare September 15, 2026 21:32
Comment thread packages/sources/cfbenchmarks/test/integration/adapter-ws.test.ts
Comment thread .gitignore
…cation v3 set

coinmetrics-lwba imports its endpoint from coinmetrics, so both must pin the
same framework copy; token-allocation aggregates its v3 adapters into a single
PriceAdapter<any>[], so all members move together for nominal type identity:
blocksize-capital, cfbenchmarks, coingecko, coinmarketcap, coinpaprika,
cryptocompare, finage, kaiko, ncfx, tiingo.

2.19.1's LwbaEndpoint.resultValidator rejects invariant-violating quotes at
cache-write time: six snapshots move from a read-path 500 to a 502, or a 200
with the last known-good quote where one is cached. cfbenchmarks' hardcoded
500 assertion becomes 502. The chaining added in 2.19.1 also lets
coinmetrics-lwba pass customOutputValidation as a constructor param again.

tiingo's stock_quotes unit test stubs adapterSettings, so it needs the new
opt-in MAX_WS_CONNECTION_AGE_SECONDS key (2.20.0, #901), matching the
framework's generator template fix.

DF-26795

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants